/*
 * Copyright (C) 2008 ProSyst Software GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.modulefusion.dirinstaller;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.modulefusion.dirinstaller.handler.ConfigFileHandler;
import org.modulefusion.dirinstaller.handler.JarFileHandler;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.service.packageadmin.PackageAdmin;
import org.osgi.util.tracker.ServiceTracker;

public final class DirWatcher {
    
    private final Logger logger;

    private final ServiceTracker packageTracker;
    private final List<? extends AbstractHandler> registeredHandlers;

    private final BundleContext context;
    private final String bundlesDir;
    private final int interval;
    private final FileFilter filter;

    private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    private final Map<String, Long> timestamps = Collections.<String, Long>synchronizedMap(new HashMap<String, Long>());

    private volatile boolean warningMissingLoadDirPresented = false;

    public DirWatcher(BundleContext context, String bundlesDir, int interval, FileFilter filter) {
        logger=Logger.getLogger(context);
        this.packageTracker = new ServiceTracker(context, PackageAdmin.class.getName(), null);
        this.packageTracker.open();

        this.context = context;
        this.bundlesDir = bundlesDir;
        this.interval = interval;
        this.filter = filter;
        this.registeredHandlers = new ArrayList<AbstractHandler>(Arrays.asList(new ConfigFileHandler(context), new JarFileHandler(context)));
    }

    public void startWatching() {
        StringBuffer msg=new StringBuffer("starting dirinstaller with following settings:").append("\n");
        msg.append("directory         : ").append(bundlesDir).append("\n");
        msg.append("poll time         : ").append(interval).append(" ").append(TimeUnit.MILLISECONDS.toString()).append("\n");
        msg.append("exclude filter    : ").append(filter.toString()).append("\n");
        msg.append("thread pool size  : ").append("1").append("\n");
        logger.info(msg.toString());
        for (AbstractHandler currentHandler : registeredHandlers) {
            currentHandler.start();
        }
        
        Runnable command = new Runnable() {

            public void run() {
                Thread.currentThread().setName("DirInstaller");
                try {
                    analyseNewState(getAllFiles(bundlesDir));
                } catch (Exception e) {
                    logger.error("got exception while watching for new files:", e);
                }
            }
        };
        executorService.scheduleAtFixedRate(command, 0, interval, TimeUnit.MILLISECONDS);
    }

    public void stopWatching() {
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
            }
            if (!executorService.isTerminated()) {
                logger.debug("waiting for termination of watching thread");
            }
        }
        for (AbstractHandler currentHandler : registeredHandlers) {
            currentHandler.stop();
        }
    }

    private List<File> getAllFiles(String dirName) {
        List<File> result=new ArrayList<File>();
        File dir = new File(dirName);
        File[] files = dir.listFiles(filter);
        if (files == null) {
            if (!warningMissingLoadDirPresented) {
                logger.warn("Directory '" + dirName + "' does not exist!");
                warningMissingLoadDirPresented = true;
            }
            return Collections.<File>emptyList();
        }

        for (File f : files) {
            try {
                if (f.isFile())
                    if (f.getName().endsWith(".cfg")) {
                        result.add(0, f);
                    } else {
                        result.add(f);
                    }
                else
                    result.addAll(getAllFiles(f.getCanonicalPath()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    private void analyseNewState(List<File> found) {
        boolean modifiedSinceLastRun=false;
        // check for new or updated bundles
        for (File file : found) {
            try {
                String path = file.getCanonicalPath();
                Long time = timestamps.get(path);

                // time == null: system startup
                // time < lastModified: updated file
                if (time == null || time < file.lastModified()) {
                    timestamps.put(path, file.lastModified());
                    modifiedSinceLastRun = true;

                    for (AbstractHandler currentHandler : registeredHandlers) {
                        if (currentHandler.handles(path)) {
                            currentHandler.installOrUpdate(path, time==null);
                        }
                    }
                }
            } catch (DirInstallerException e) {
                logger.error("Problems installing or updating bundle. File: " + file.getName(), e);
            } catch (IOException e) {
                logger.error("Problems accessing bundle. File: " + file.getName(), e);
            }
        }

        // check removed bundles
        Iterator<String> it = timestamps.keySet().iterator();
        while (it.hasNext()) {
            String s = it.next();
            try {
                if (!isStringInFileList(s, found)) {
                    for (Bundle b : context.getBundles()) {
                        if (b.getLocation().equals("file:" + s)) {
                            logger.info("Removing bundle '" + b.getSymbolicName() + "'");
                            b.uninstall();
                            modifiedSinceLastRun = true;
                        }
                    }
                    it.remove();
                    timestamps.remove(s);
                }
            } catch (BundleException e) {
                logger.error("Problems uninstalling bundle: ", e);
            } catch (IOException e) {
                logger.error("Problems processing file: ", e);
            }
        }

        if (modifiedSinceLastRun)
            startAllAndRefresh();
    }

    private boolean isStringInFileList(String string, List<File> fileList) throws IOException {
        for (File f : fileList) {
            if (f.getCanonicalPath().equals(string))
                return true;
        }
        return false;
    }

    private void startAllAndRefresh() {
        for (Bundle b : context.getBundles()) {
            try {
                if (b.getState() != Bundle.ACTIVE && !isFragment(b)) {
                    b.start();
                }
            } catch (BundleException e) {
                logger.error("Problems starting bundle: " + b, e);
            }
        }
        Object o = this.packageTracker.getService();
        if (o != null) {
            PackageAdmin admin = (PackageAdmin) o;
            admin.refreshPackages(null);
        } else {
            logger.error("Can not refresh packages. No PackageAdmin service registered");
        }
    }

    private boolean isFragment(Bundle b) {
        return b.getHeaders().get("Fragment-Host") != null;
    }
}
